import * as Cesium from "cesium";
import Sandcastle from "Sandcastle";

const viewer = new Cesium.Viewer("cesiumContainer", {
  baseLayer: Cesium.ImageryLayer.fromProviderAsync(
    Cesium.TileMapServiceImageryProvider.fromUrl(
      Cesium.buildModuleUrl("Assets/Textures/NaturalEarthII"),
    ),
  ),
  baseLayerPicker: false,
  geocoder: false,
  animation: false,
  timeline: false,
  projectionPicker: true,
  sceneModePicker: false,
});

viewer.extend(Cesium.viewerVoxelInspectorMixin);
viewer.scene.debugShowFramesPerSecond = true;

const globalTransform = Cesium.Matrix4.fromScale(
  Cesium.Cartesian3.fromElements(
    Cesium.Ellipsoid.WGS84.maximumRadius,
    Cesium.Ellipsoid.WGS84.maximumRadius,
    Cesium.Ellipsoid.WGS84.maximumRadius,
  ),
);

function ProceduralSingleTileVoxelProvider(shape) {
  this.shape = shape;
  this.minBounds = Cesium.VoxelShapeType.getMinBounds(shape).clone();
  this.maxBounds = Cesium.VoxelShapeType.getMaxBounds(shape).clone();
  this.dimensions = new Cesium.Cartesian3(8, 8, 8);
  this.names = ["color"];
  this.types = [Cesium.MetadataType.VEC4];
  this.componentTypes = [Cesium.MetadataComponentType.FLOAT32];
  this.globalTransform = globalTransform;
}

const scratchColor = new Cesium.Color();

ProceduralSingleTileVoxelProvider.prototype.requestData = function (options) {
  if (options.tileLevel >= 1) {
    return Promise.reject(`No tiles available beyond level 0`);
  }

  const dimensions = this.dimensions;
  const voxelCount = dimensions.x * dimensions.y * dimensions.z;
  const type = this.types[0];
  const channelCount = Cesium.MetadataType.getComponentCount(type);
  const dataColor = new Float32Array(voxelCount * channelCount);

  const randomSeed = dimensions.y * dimensions.x + dimensions.x;
  Cesium.Math.setRandomNumberSeed(randomSeed);
  const hue = Cesium.Math.nextRandomNumber();

  for (let z = 0; z < dimensions.z; z++) {
    for (let y = 0; y < dimensions.y; y++) {
      const indexZY = z * dimensions.y * dimensions.x + y * dimensions.x;
      for (let x = 0; x < dimensions.x; x++) {
        const lerperX = x / (dimensions.x - 1);
        const lerperY = y / (dimensions.y - 1);
        const lerperZ = z / (dimensions.z - 1);

        const h = hue + lerperX * 0.5 - lerperY * 0.3 + lerperZ * 0.2;
        const s = 1.0 - lerperY * 0.2;
        const v = 0.5 + 2.0 * (lerperZ - 0.5) * 0.2;
        const color = Cesium.Color.fromHsl(h, s, v, 1.0, scratchColor);

        const index = (indexZY + x) * channelCount;
        dataColor[index + 0] = color.red;
        dataColor[index + 1] = color.green;
        dataColor[index + 2] = color.blue;
        dataColor[index + 3] = 0.75;
      }
    }
  }

  const content = Cesium.VoxelContent.fromMetadataArray([dataColor]);
  return Promise.resolve(content);
};

function ProceduralMultiTileVoxelProvider(shape) {
  this.shape = shape;
  this.minBounds = Cesium.VoxelShapeType.getMinBounds(shape).clone();
  this.maxBounds = Cesium.VoxelShapeType.getMaxBounds(shape).clone();
  this.dimensions = new Cesium.Cartesian3(4, 4, 4);
  this.paddingBefore = new Cesium.Cartesian3(1, 1, 1);
  this.paddingAfter = new Cesium.Cartesian3(1, 1, 1);
  this.names = ["color"];
  this.types = [Cesium.MetadataType.VEC4];
  this.componentTypes = [Cesium.MetadataComponentType.FLOAT32];
  this.globalTransform = globalTransform;

  this.availableLevels = 2;
  this._allVoxelData = new Array(this.availableLevels);

  const allVoxelData = this._allVoxelData;
  const channelCount = Cesium.MetadataType.getComponentCount(this.types[0]);
  const { dimensions } = this;

  for (let level = 0; level < this.availableLevels; level++) {
    const dimAtLevel = Math.pow(2, level);
    const voxelCountX = dimensions.x * dimAtLevel;
    const voxelCountY = dimensions.y * dimAtLevel;
    const voxelCountZ = dimensions.z * dimAtLevel;
    const voxelsPerLevel = voxelCountX * voxelCountY * voxelCountZ;
    const levelData = (allVoxelData[level] = new Array(
      voxelsPerLevel * channelCount,
    ));

    for (let z = 0; z < voxelCountX; z++) {
      for (let y = 0; y < voxelCountY; y++) {
        const indexZY = z * voxelCountY * voxelCountX + y * voxelCountX;
        for (let x = 0; x < voxelCountZ; x++) {
          const index = (indexZY + x) * channelCount;
          levelData[index + 0] = x / (voxelCountX - 1);
          levelData[index + 1] = y / (voxelCountY - 1);
          levelData[index + 2] = z / (voxelCountZ - 1);
          levelData[index + 3] = 0.5;
        }
      }
    }
  }
}

ProceduralMultiTileVoxelProvider.prototype.requestData = function (options) {
  const { tileLevel, tileX, tileY, tileZ } = options;

  if (tileLevel >= this.availableLevels) {
    return Promise.reject(
      `No tiles available beyond level ${this.availableLevels - 1}`,
    );
  }

  const type = this.types[0];
  const channelCount = Cesium.MetadataType.getComponentCount(type);
  const { dimensions, paddingBefore, paddingAfter } = this;
  const paddedDimensions = Cesium.Cartesian3.fromElements(
    dimensions.x + paddingBefore.x + paddingAfter.x,
    dimensions.y + paddingBefore.y + paddingAfter.y,
    dimensions.z + paddingBefore.z + paddingAfter.z,
  );
  const dimAtLevel = Math.pow(2, tileLevel);
  const dimensionsGlobal = Cesium.Cartesian3.fromElements(
    dimensions.x * dimAtLevel,
    dimensions.y * dimAtLevel,
    dimensions.z * dimAtLevel,
  );
  const minimumGlobalCoord = Cesium.Cartesian3.ZERO;
  const maximumGlobalCoord = new Cesium.Cartesian3(
    dimensionsGlobal.x - 1,
    dimensionsGlobal.y - 1,
    dimensionsGlobal.z - 1,
  );
  let coordGlobal = new Cesium.Cartesian3();

  const dataGlobal = this._allVoxelData;
  const dataTile = new Float32Array(
    paddedDimensions.x * paddedDimensions.y * paddedDimensions.z * channelCount,
  );

  for (let z = 0; z < paddedDimensions.z; z++) {
    const indexZ = z * paddedDimensions.y * paddedDimensions.x;
    for (let y = 0; y < paddedDimensions.y; y++) {
      const indexZY = indexZ + y * paddedDimensions.x;
      for (let x = 0; x < paddedDimensions.x; x++) {
        const indexTile = indexZY + x;

        coordGlobal = Cesium.Cartesian3.clamp(
          Cesium.Cartesian3.fromElements(
            tileX * dimensions.x + (x - paddingBefore.x),
            tileY * dimensions.y + (y - paddingBefore.y),
            tileZ * dimensions.z + (z - paddingBefore.z),
            coordGlobal,
          ),
          minimumGlobalCoord,
          maximumGlobalCoord,
          coordGlobal,
        );

        const indexGlobal =
          coordGlobal.z * dimensionsGlobal.y * dimensionsGlobal.x +
          coordGlobal.y * dimensionsGlobal.x +
          coordGlobal.x;

        for (let c = 0; c < channelCount; c++) {
          dataTile[indexTile * channelCount + c] =
            dataGlobal[tileLevel][indexGlobal * channelCount + c];
        }
      }
    }
  }
  const content = Cesium.VoxelContent.fromMetadataArray([dataTile]);
  return Promise.resolve(content);
};

function createPrimitive(provider, customShader) {
  viewer.scene.primitives.removeAll();

  const voxelPrimitive = viewer.scene.primitives.add(
    new Cesium.VoxelPrimitive({
      provider: provider,
      customShader: customShader,
    }),
  );

  viewer.voxelInspector.viewModel.voxelPrimitive = voxelPrimitive;
  viewer.camera.flyToBoundingSphere(voxelPrimitive.boundingSphere, {
    duration: 0.0,
  });

  return voxelPrimitive;
}

const customShaderColor = new Cesium.CustomShader({
  fragmentShaderText: `void fragmentMain(FragmentInput fsInput, inout czm_modelMaterial material)
  {
      material.diffuse = fsInput.metadata.color.rgb;
      float transparency = 1.0 - fsInput.metadata.color.a;

      // To mimic light scattering, use exponential decay
      float thickness = fsInput.voxel.travelDistance * 16.0;
      material.alpha = 1.0 - pow(transparency, thickness);
  }`,
});

Sandcastle.addToolbarMenu([
  {
    text: "Ellipsoid - Procedural Tile",
    onselect: function () {
      const provider = new ProceduralSingleTileVoxelProvider(
        Cesium.VoxelShapeType.ELLIPSOID,
      );
      provider.minBounds.z = 0.0;
      provider.maxBounds.z = 1000000.0;
      createPrimitive(provider, customShaderColor);
    },
  },
  {
    text: "Cylinder - Procedural Tile",
    onselect: function () {
      const provider = new ProceduralSingleTileVoxelProvider(
        Cesium.VoxelShapeType.CYLINDER,
      );
      createPrimitive(provider, customShaderColor);
    },
  },
  {
    text: "Box - Procedural Tile",
    onselect: function () {
      const provider = new ProceduralSingleTileVoxelProvider(
        Cesium.VoxelShapeType.BOX,
      );
      createPrimitive(provider, customShaderColor);
    },
  },
  {
    text: "Box - Procedural Tileset",
    onselect: function () {
      const provider = new ProceduralMultiTileVoxelProvider(
        Cesium.VoxelShapeType.BOX,
      );
      createPrimitive(provider, customShaderColor);
    },
  },
  {
    text: "Ellipsoid - Procedural Tileset",
    onselect: function () {
      const provider = new ProceduralMultiTileVoxelProvider(
        Cesium.VoxelShapeType.ELLIPSOID,
      );
      provider.minBounds.z = 0.0;
      provider.maxBounds.z = 1000000.0;
      createPrimitive(provider, customShaderColor);
    },
  },
  {
    text: "Cylinder - Procedural Tileset",
    onselect: function () {
      const provider = new ProceduralMultiTileVoxelProvider(
        Cesium.VoxelShapeType.CYLINDER,
      );
      createPrimitive(provider, customShaderColor);
    },
  },
]);

const handler = new Cesium.ScreenSpaceEventHandler(viewer.scene.canvas);
handler.setInputAction(function (movement) {
  const scene = viewer.scene;
  const mousePosition = movement.position;
  const pickedPrimitive = scene.pick(mousePosition);
  console.log(pickedPrimitive);
}, Cesium.ScreenSpaceEventType.LEFT_CLICK);
